diff --git a/.github/actions/private-ecr-login/action.yml b/.github/actions/private-ecr-login/action.yml new file mode 100644 index 0000000..774a645 --- /dev/null +++ b/.github/actions/private-ecr-login/action.yml @@ -0,0 +1,39 @@ +name: 'Log into Private ECR' +description: 'Github OIDC auth and assume role into account, then use AWS ECR Login action' +inputs: + aws_ci_account: + description: 'AWS Account ID for CI' + required: false + default: 824635284302 + aws_user_account: + description: 'AWS Account ID for users' + required: false + default: 265299512749 +outputs: + registry: + description: "ECR Registry" + value: ${{ steps.login-ecr.outputs.registry }} +runs: + using: "composite" + steps: + - name: assume oidc role + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-to-assume: arn:aws:iam::${{ inputs.aws_user_account }}:role/ci-oidc-role + role-session-name: github-actions-oidc + role-duration-seconds: 900 + - name: assume target role + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }} + aws-session-token: ${{ env.AWS_SESSION_TOKEN }} + aws-region: us-west-2 + role-to-assume: arn:aws:iam::${{ inputs.aws_ci_account }}:role/ci-role + role-session-name: github-actions-private-ecr + role-duration-seconds: 900 + + - name: Login to Private ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 \ No newline at end of file diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 0000000..13a7b18 --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,174 @@ +name: build-and-push +env: + LIVEPOLL_ECR_REPOSITORY: livepoll + PANDOC_ECR_REPOSITORY: pandoc +on: + pull_request: + types: [ opened, synchronize, reopened ] + push: + branches: + - main + - rc/** + +jobs: + validate-terraform: + runs-on: ubuntu-latest + name: Validate Terraform + timeout-minutes: 5 + permissions: + id-token: write # Used for AWS OIDC auth + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + name: Checkout project + + - name: OIDC Auth to AWS + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-to-assume: arn:aws:iam::${{ secrets.aws_user_account }}:role/ci-oidc-role + role-session-name: github-actions + role-duration-seconds: 900 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.9.8 + + - name: Livepoll Terraform Init + run: terraform init -backend=false + working-directory: deploy/terraform/livepoll + + - name: Livepoll Terraform Format + run: terraform fmt -check + working-directory: deploy/terraform/livepoll + + - name: Livepoll Terraform Validate + id: validate + run: terraform validate -no-color + working-directory: deploy/terraform/livepoll + + - name: Pandoc Terraform Init + run: terraform init -backend=false + working-directory: deploy/terraform/pandoc + + - name: Pandoc Terraform Format + run: terraform fmt -check + working-directory: deploy/terraform/pandoc + + - name: Pandoc Terraform Validate + run: terraform validate -no-color + working-directory: deploy/terraform/pandoc + + build-and-push-livepoll: + runs-on: ubuntu-latest + name: Build and Push Livepoll Docker Image + timeout-minutes: 30 + needs: [validate-terraform] + permissions: + id-token: write # Used for AWS OIDC auth + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + name: Checkout project + + - id: read_tree_hash + name: Read git tree hash + run: | + tree_hash=$(git rev-parse HEAD:) + echo "tree_hash=$tree_hash" >> $GITHUB_OUTPUT + + - id: set_branch_name + name: Read git branch name + run: | + branch_name=${GITHUB_REF##*/} + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT + + - uses: ./.github/actions/private-ecr-login + name: Login to Private ECR + id: login-ecr + + - name: Check for prebuilt image + id: prebuilt_check + run: | + docker manifest inspect ${{ steps.login-ecr.outputs.registry }}/${{ env.LIVEPOLL_ECR_REPOSITORY }}:gha-tree-${{ steps.read_tree_hash.outputs.tree_hash }} || echo "image_exists=$?" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + if: steps.prebuilt_check.outputs.image_exists != 0 + + - name: Docker build and push + uses: docker/build-push-action@v5 + if: steps.prebuilt_check.outputs.image_exists != 0 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + with: + context: . + file: docker/Dockerfile-livepoll + platforms: linux/amd64 + push: true + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ env.LIVEPOLL_ECR_REPOSITORY }}:gha-commit-${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.LIVEPOLL_ECR_REPOSITORY }}:gha-tree-${{ steps.read_tree_hash.outputs.tree_hash }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.LIVEPOLL_ECR_REPOSITORY }}:gha-build-${{ steps.set_branch_name.outputs.branch_name }}-${{ github.run_number }}-${{ github.run_attempt }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.LIVEPOLL_ECR_REPOSITORY }}:gha-build-${{ steps.set_branch_name.outputs.branch_name }}-latest + cache-from: type=gha + cache-to: type=gha,mode=max + + build-and-push-pandoc: + runs-on: ubuntu-latest + name: Build and Push Pandoc Docker Image + timeout-minutes: 30 + needs: [validate-terraform] + permissions: + id-token: write # Used for AWS OIDC auth + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + name: Checkout project + + - id: read_tree_hash + name: Read git tree hash + run: | + tree_hash=$(git rev-parse HEAD:) + echo "tree_hash=$tree_hash" >> $GITHUB_OUTPUT + + - id: set_branch_name + name: Read git branch name + run: | + branch_name=${GITHUB_REF##*/} + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT + + - uses: ./.github/actions/private-ecr-login + name: Login to Private ECR + id: login-ecr + + - name: Check for prebuilt image + id: prebuilt_check + run: | + docker manifest inspect ${{ steps.login-ecr.outputs.registry }}/${{ env.PANDOC_ECR_REPOSITORY }}:gha-tree-${{ steps.read_tree_hash.outputs.tree_hash }} || echo "image_exists=$?" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + if: steps.prebuilt_check.outputs.image_exists != 0 + + - name: Docker build and push + uses: docker/build-push-action@v5 + if: steps.prebuilt_check.outputs.image_exists != 0 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + with: + context: . + file: docker/Dockerfile-pandoc + platforms: linux/amd64 + push: true + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ env.PANDOC_ECR_REPOSITORY }}:gha-commit-${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.PANDOC_ECR_REPOSITORY }}:gha-tree-${{ steps.read_tree_hash.outputs.tree_hash }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.PANDOC_ECR_REPOSITORY }}:gha-build-${{ steps.set_branch_name.outputs.branch_name }}-${{ github.run_number }}-${{ github.run_attempt }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.PANDOC_ECR_REPOSITORY }}:gha-build-${{ steps.set_branch_name.outputs.branch_name }}-latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/manual-deploy.yml b/.github/workflows/manual-deploy.yml new file mode 100644 index 0000000..d0d5985 --- /dev/null +++ b/.github/workflows/manual-deploy.yml @@ -0,0 +1,30 @@ +name: manual-deploy +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment' + required: true + default: 'dev' + type: choice + options: + - dev + - staging + project: + description: 'Project' + required: true + default: 'livepoll' + type: choice + options: + - livepoll + - pandoc +jobs: + ecs-deploy: + name: ECS Deployment + uses: ./.github/workflows/shared-deploy.yml + with: + environment: ${{ github.event.inputs.environment }} + project: ${{ github.event.inputs.project }} + secrets: + aws_user_account: ${{ secrets.AWS_USER_ACCOUNT }} + aws_ci_account: ${{ secrets.AWS_CI_ACCOUNT }} \ No newline at end of file diff --git a/.github/workflows/shared-deploy.yml b/.github/workflows/shared-deploy.yml new file mode 100644 index 0000000..770bf3b --- /dev/null +++ b/.github/workflows/shared-deploy.yml @@ -0,0 +1,100 @@ +name: shared-deployment-workflow +permissions: + id-token: write # Used for AWS OIDC auth + contents: read # This is required for actions/checkout +on: + workflow_call: + inputs: + environment: + description: 'Environment name passed from the caller workflow' + required: true + type: string + project: + description: 'Project name passed from the caller workflow' + required: true + type: string + secrets: + aws_user_account: + description: 'AWS Account ID for IAM users' + required: true + aws_ci_account: + description: 'AWS Account ID for IAM users' + required: true + +jobs: + terraform-deploy: + name: Terraform Deployment + uses: ./.github/workflows/terraform-deploy.yml + with: + environment: ${{ inputs.environment }} + project: ${{ inputs.project }} + secrets: + aws_user_account: ${{ secrets.AWS_USER_ACCOUNT }} + aws_ci_account: ${{ secrets.AWS_CI_ACCOUNT }} + + ecs-deploy: + name: ECS Deployment + timeout-minutes: 30 + runs-on: ubuntu-latest + needs: [terraform-deploy] + steps: + - uses: actions/checkout@v4 + name: Checkout project + + - uses: ./.github/actions/private-ecr-login + name: Login to Private ECR + id: login-ecr + + - id: read_env_json + name: Read Environment JSON + run: | + env_json=$(jq -c '.environments[] | select(.environment_label=="${{ inputs.environment }}")' ./deploy/${{ inputs.project }}-environments.json) + echo "env_json=$env_json" >> $GITHUB_OUTPUT + - id: set_env_metadata + name: Set Environment Metadata + run: | + echo "account=${{ fromJSON(steps.read_env_json.outputs.env_json).account }}" >> $GITHUB_OUTPUT + echo "ssm_prefix=${{ fromJSON(steps.read_env_json.outputs.env_json).ssm_prefix }}" >> $GITHUB_OUTPUT + echo "webapp_ssm_prefix=${{ fromJSON(steps.read_env_json.outputs.env_json).webapp.ssm_prefix }}" >> $GITHUB_OUTPUT + echo "shoryuken_ssm_prefix=${{ fromJSON(steps.read_env_json.outputs.env_json).shoryuken.ssm_prefix }}" >> $GITHUB_OUTPUT + echo "cluster_name=${{ fromJSON(steps.read_env_json.outputs.env_json).fargate.cluster_name }}" >> $GITHUB_OUTPUT + + - name: OIDC Auth to AWS + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-to-assume: arn:aws:iam::${{ secrets.aws_user_account }}:role/ci-oidc-role + role-session-name: github-actions + role-duration-seconds: 900 + + - name: Assume role in target account + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }} + aws-session-token: ${{ env.AWS_SESSION_TOKEN }} + aws-region: us-west-2 + role-to-assume: arn:aws:iam::${{ steps.set_env_metadata.outputs.account }}:role/ci-role + role-session-name: github-actions + role-duration-seconds: 1200 + + - id: read_tree_hash + name: Read git tree hash + run: | + tree_hash=$(git rev-parse HEAD:) + echo "tree_hash=$tree_hash" >> $GITHUB_OUTPUT + + - name: Verify image + run: | + if ! docker manifest inspect ${{ steps.login-ecr.outputs.registry }}/${{ inputs.project }}:gha-tree-${{ steps.read_tree_hash.outputs.tree_hash }}; then + echo "If this is a PR build, you may need to pull in changes from the target branch into your PR branch." + exit 1 + fi + + - name: Run deploy script + run: | + deploy/ecs_deploy.sh \ + --cluster-name ${{ steps.set_env_metadata.outputs.cluster_name }} \ + --ssm-prefix ${{ steps.set_env_metadata.outputs.ssm_prefix }} \ + --account-number ${{ secrets.aws_ci_account }} \ + --project-name ${{ inputs.project }} \ No newline at end of file diff --git a/.github/workflows/tag-deploy-livepoll.yml b/.github/workflows/tag-deploy-livepoll.yml new file mode 100644 index 0000000..12f8351 --- /dev/null +++ b/.github/workflows/tag-deploy-livepoll.yml @@ -0,0 +1,16 @@ +name: tag-deploy +on: + push: + tags: + - release/livepoll/* + +jobs: + ecs-deploy: + name: ECS Deployment + uses: ./.github/workflows/shared-deploy.yml + with: + environment: prod + project: livepoll + secrets: + aws_user_account: ${{ secrets.AWS_USER_ACCOUNT }} + aws_ci_account: ${{ secrets.AWS_CI_ACCOUNT }} \ No newline at end of file diff --git a/.github/workflows/tag-deploy-pandoc.yml b/.github/workflows/tag-deploy-pandoc.yml new file mode 100644 index 0000000..cd0ba9d --- /dev/null +++ b/.github/workflows/tag-deploy-pandoc.yml @@ -0,0 +1,16 @@ +name: tag-deploy +on: + push: + tags: + - release/pandoc/* + +jobs: + ecs-deploy: + name: ECS Deployment + uses: ./.github/workflows/shared-deploy.yml + with: + environment: prod + project: pandoc + secrets: + aws_user_account: ${{ secrets.AWS_USER_ACCOUNT }} + aws_ci_account: ${{ secrets.AWS_CI_ACCOUNT }} \ No newline at end of file diff --git a/.github/workflows/terraform-deploy.yml b/.github/workflows/terraform-deploy.yml new file mode 100644 index 0000000..b098c08 --- /dev/null +++ b/.github/workflows/terraform-deploy.yml @@ -0,0 +1,91 @@ +name: terraform-deployment-workflow +permissions: + id-token: write # Used for AWS OIDC auth + contents: read # This is required for actions/checkout +on: + workflow_call: + inputs: + environment: + description: 'Environment name passed from the caller workflow' + required: true + type: string + project: + description: 'Project name passed from the caller workflow' + required: true + type: string + secrets: + aws_user_account: + description: 'AWS Account ID for IAM users' + required: true + aws_ci_account: + description: 'AWS Account ID for IAM users' + required: true + +jobs: + # this job gets a password for public ECR to use in future steps + terraform-deploy: + name: Terraform Deployment + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + name: Checkout project + + - id: read_env_json + name: Read Environment JSON + run: | + env_json=$(jq -c '.environments[] | select(.environment_label=="${{ inputs.environment }}")' ./deploy/${{ inputs.project }}-environments.json) + echo "env_json=$env_json" >> $GITHUB_OUTPUT + + - id: set_env_metadata + name: Set Environment Metadata + run: | + echo "account=${{ fromJSON(steps.read_env_json.outputs.env_json).account }}" >> $GITHUB_OUTPUT + echo "region=${{ fromJSON(steps.read_env_json.outputs.env_json).region }}" >> $GITHUB_OUTPUT + echo "terraform_state_key=${{ fromJSON(steps.read_env_json.outputs.env_json).terraform_state_key }}" >> $GITHUB_OUTPUT + echo "ssm_prefix=${{ fromJSON(steps.read_env_json.outputs.env_json).ssm_prefix }}" >> $GITHUB_OUTPUT + echo "cluster_name=${{ fromJSON(steps.read_env_json.outputs.env_json).fargate.cluster_name }}" >> $GITHUB_OUTPUT + + + - name: OIDC Auth to AWS + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-west-2 + role-to-assume: arn:aws:iam::${{ secrets.aws_user_account }}:role/ci-oidc-role + role-session-name: github-actions + role-duration-seconds: 900 + + # Based on https://developer.hashicorp.com/terraform/tutorials/automation/github-actions + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.9.8 + + - name: Terraform Init + id: init + run: terraform init -backend-config=key=${{ steps.set_env_metadata.outputs.terraform_state_key }} + working-directory: deploy/terraform/${{ inputs.project }} + + - name: Terraform Format + id: fmt + run: terraform fmt -check + working-directory: deploy/terraform/${{ inputs.project }} + + - name: Terraform Validate + id: validate + run: terraform validate -no-color + working-directory: deploy/terraform/${{ inputs.project }} + + - name: Terraform Plan + id: plan + run: | + terraform plan -no-color -input=false -out terraform.tfplan \ + -var environment=${{ inputs.environment }} \ + -var account=${{ steps.set_env_metadata.outputs.account }} \ + -var region=${{ steps.set_env_metadata.outputs.region }} + working-directory: deploy/terraform/${{ inputs.project }} + + - name: Terraform Apply + id: apply + run: terraform apply -auto-approve -input=false terraform.tfplan + working-directory: deploy/terraform/${{ inputs.project }} diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml new file mode 100644 index 0000000..8780537 --- /dev/null +++ b/.github/workflows/terraform.yml @@ -0,0 +1,31 @@ +name: terraform-deploy +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment' + required: true + default: 'dev' + type: choice + options: + - dev + - staging + - prod + project: + description: 'Project' + required: true + default: 'livepoll' + type: choice + options: + - livepoll + - pandoc +jobs: + terraform-deploy: + name: Terraform Deployment + uses: ./.github/workflows/terraform-deploy.yml + with: + environment: ${{ github.event.inputs.environment }} + project: ${{ github.event.inputs.project }} + secrets: + aws_user_account: ${{ secrets.AWS_USER_ACCOUNT }} + aws_ci_account: ${{ secrets.AWS_CI_ACCOUNT }} diff --git a/deploy/ecs_deploy.sh b/deploy/ecs_deploy.sh new file mode 100755 index 0000000..9c0cc92 --- /dev/null +++ b/deploy/ecs_deploy.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# +# This script: +# - deploys a docker image to an ECS environment +# +# Required environment variables: +# - GITHUB_WORKFLOW - provided by GHA +# - GITHUB_RUN_NUMBER - provided by GHA +# - GITHUB_RUN_ATTEMPT - provided by GHA +# - GITHUB_SHA - provided by GHA + +set -euox pipefail + +export AWS_ECR_IMAGE_TAG=gha-tree-$(git rev-parse HEAD:) +export VERSION_LABEL=gha-build-$GITHUB_WORKFLOW-$GITHUB_RUN_NUMBER-$GITHUB_RUN_ATTEMPT + +while [[ $# -gt 0 ]]; do + case $1 in + -c|--cluster-name) + ECS_CLUSTER_NAME="$2" + shift # past argument + shift # past value + ;; + -s|--ssm-prefix) + SSM_PREFIX="$2" + shift # past argument + shift # past value + ;; + -n|--account-number) + AWS_CI_ACCOUNT="$2" + shift # past argument + shift # past value + ;; + -p|--project-name) + PROJECT_NAME="$2" + shift + shift + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +ECS_SERVICE_NAME="${ECS_CLUSTER_NAME}" +ECR_REPOSITORY="${PROJECT_NAME}" +CONTAINER_NAME="${PROJECT_NAME}" + +echo -e "\n============================================================" +echo " Beginning deployment to AWS Fargate environment: $ECS_CLUSTER_NAME $ECS_SERVICE_NAME" +echo -e "============================================================\n" + +# Deploy the latest image to Fargate for the webapp +docker run -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \ + fabfuel/ecs-deploy:1.15.0 ecs deploy --region us-west-2 --no-deregister --timeout 600 --rollback \ + -i ${CONTAINER_NAME} $AWS_CI_ACCOUNT.dkr.ecr.us-west-2.amazonaws.com/$ECR_REPOSITORY:$AWS_ECR_IMAGE_TAG \ + -e ${CONTAINER_NAME} PARAMETER_STORE_PREFIX_LIST ${SSM_PREFIX} \ + -e ${CONTAINER_NAME} AWS_REGION us-west-2 \ + $ECS_CLUSTER_NAME $ECS_SERVICE_NAME diff --git a/deploy/livepoll-environments.json b/deploy/livepoll-environments.json new file mode 100644 index 0000000..ffb7ef0 --- /dev/null +++ b/deploy/livepoll-environments.json @@ -0,0 +1,43 @@ +{ + "environments": [ + { + "environment_label": "dev", + "account": 725843923591, + "region": "us-west-2", + "terraform_state_key": "accounts/lumen-stage/us-west-2/dev/livepoll-app/config/terraform.tfstate", + "ssm_prefix": "/lumen-services/us-west-2/dev/livepoll", + "fargate": { + "cluster_name": "dev-livepoll" + }, + "webapp": { + "ssm_prefix": "/lumen-services/us-west-2/dev/livepoll/webapp" + } + }, + { + "environment_label": "staging", + "account": 725843923591, + "region": "us-west-2", + "terraform_state_key": "accounts/lumen-stage/us-west-2/staging/livepoll-app/config/terraform.tfstate", + "ssm_prefix": "/lumen-services/us-west-2/staging/livepoll", + "fargate": { + "cluster_name": "staging-livepoll" + }, + "webapp": { + "ssm_prefix": "/lumen-services/us-west-2/staging/livepoll/webapp" + } + }, + { + "environment_label": "prod", + "account": 523740042085, + "region": "us-west-2", + "terraform_state_key": "accounts/lumen-prod/us-west-2/prod/livepoll-app/config/terraform.tfstate", + "ssm_prefix": "/lumen-services/us-west-2/prod/livepoll", + "fargate": { + "cluster_name": "prod-livepoll" + }, + "webapp": { + "ssm_prefix": "/lumen-services/us-west-2/prod/livepoll/webapp" + } + } + ] +} \ No newline at end of file diff --git a/deploy/pandoc-environments.json b/deploy/pandoc-environments.json new file mode 100644 index 0000000..f742786 --- /dev/null +++ b/deploy/pandoc-environments.json @@ -0,0 +1,43 @@ +{ + "environments": [ + { + "environment_label": "dev", + "account": 725843923591, + "region": "us-west-2", + "terraform_state_key": "accounts/lumen-stage/us-west-2/dev/pandoc-app/config/terraform.tfstate", + "ssm_prefix": "/lumen-services/us-west-2/dev/pandoc", + "fargate": { + "cluster_name": "dev-pandoc" + }, + "webapp": { + "ssm_prefix": "/lumen-services/us-west-2/dev/pandoc/webapp" + } + }, + { + "environment_label": "staging", + "account": 725843923591, + "region": "us-west-2", + "terraform_state_key": "accounts/lumen-stage/us-west-2/staging/pandoc-app/config/terraform.tfstate", + "ssm_prefix": "/lumen-services/us-west-2/staging/pandoc", + "fargate": { + "cluster_name": "staging-pandoc" + }, + "webapp": { + "ssm_prefix": "/lumen-services/us-west-2/staging/pandoc/webapp" + } + }, + { + "environment_label": "prod", + "account": 523740042085, + "region": "us-west-2", + "terraform_state_key": "accounts/lumen-prod/us-west-2/prod/pandoc-app/config/terraform.tfstate", + "ssm_prefix": "/lumen-services/us-west-2/prod/pandoc", + "fargate": { + "cluster_name": "prod-pandoc" + }, + "webapp": { + "ssm_prefix": "/lumen-services/us-west-2/prod/pandoc/webapp" + } + } + ] +} \ No newline at end of file diff --git a/deploy/terraform/livepoll/_locals.tf b/deploy/terraform/livepoll/_locals.tf new file mode 100644 index 0000000..d80b489 --- /dev/null +++ b/deploy/terraform/livepoll/_locals.tf @@ -0,0 +1,5 @@ +locals { + service_name = "livepoll" + parameter_store_prefix = "/lumen-services/${var.region}/${var.environment}/${local.service_name}" + parameter_store_prefix_webapp = "${local.parameter_store_prefix}/webapp" +} \ No newline at end of file diff --git a/deploy/terraform/livepoll/_provider.tf b/deploy/terraform/livepoll/_provider.tf new file mode 100644 index 0000000..63e3723 --- /dev/null +++ b/deploy/terraform/livepoll/_provider.tf @@ -0,0 +1,34 @@ +provider "aws" { + region = var.region + + assume_role { + role_arn = "arn:aws:iam::${var.account}:role/ci-role" + } + + default_tags { + tags = { + Environment = var.environment + Terraform = "true" + Terraform_Project = local.service_name + Service = local.service_name + } + } +} + +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } + backend "s3" { + encrypt = true + bucket = "lumen-terraform-state" + dynamodb_table = "lumen-terraform-state-lock" + region = "us-west-2" + role_arn = "arn:aws:iam::824635284302:role/terraform" + } +} diff --git a/deploy/terraform/livepoll/parameter_store.tf b/deploy/terraform/livepoll/parameter_store.tf new file mode 100644 index 0000000..8f8456c --- /dev/null +++ b/deploy/terraform/livepoll/parameter_store.tf @@ -0,0 +1,19 @@ +resource "aws_ssm_parameter" "LIVEPOLL_USE_INSECURE_HTTP" { + name = "${local.parameter_store_prefix}/LIVEPOLL_USE_INSECURE_HTTP" + type = "SecureString" + value = "true" # initial value only, updates not handled in terraform + + lifecycle { + ignore_changes = [value] + } +} + +resource "aws_ssm_parameter" "LIVEPOLL_PASSWORD" { + name = "${local.parameter_store_prefix}/LIVEPOLL_PASSWORD" + type = "SecureString" + value = "replace_me" # initial value only, updates not handled in terraform + + lifecycle { + ignore_changes = [value] + } +} \ No newline at end of file diff --git a/deploy/terraform/livepoll/variables.tf b/deploy/terraform/livepoll/variables.tf new file mode 100644 index 0000000..e960b8d --- /dev/null +++ b/deploy/terraform/livepoll/variables.tf @@ -0,0 +1,11 @@ +variable "environment" { + type = string +} + +variable "region" { + type = string +} + +variable "account" { + type = string +} diff --git a/deploy/terraform/pandoc/_locals.tf b/deploy/terraform/pandoc/_locals.tf new file mode 100644 index 0000000..e228494 --- /dev/null +++ b/deploy/terraform/pandoc/_locals.tf @@ -0,0 +1,5 @@ +locals { + service_name = "pandoc" + parameter_store_prefix = "/lumen-services/${var.region}/${var.environment}/${local.service_name}" + parameter_store_prefix_webapp = "${local.parameter_store_prefix}/webapp" +} \ No newline at end of file diff --git a/deploy/terraform/pandoc/_provider.tf b/deploy/terraform/pandoc/_provider.tf new file mode 100644 index 0000000..63e3723 --- /dev/null +++ b/deploy/terraform/pandoc/_provider.tf @@ -0,0 +1,34 @@ +provider "aws" { + region = var.region + + assume_role { + role_arn = "arn:aws:iam::${var.account}:role/ci-role" + } + + default_tags { + tags = { + Environment = var.environment + Terraform = "true" + Terraform_Project = local.service_name + Service = local.service_name + } + } +} + +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } + backend "s3" { + encrypt = true + bucket = "lumen-terraform-state" + dynamodb_table = "lumen-terraform-state-lock" + region = "us-west-2" + role_arn = "arn:aws:iam::824635284302:role/terraform" + } +} diff --git a/deploy/terraform/pandoc/variables.tf b/deploy/terraform/pandoc/variables.tf new file mode 100644 index 0000000..e960b8d --- /dev/null +++ b/deploy/terraform/pandoc/variables.tf @@ -0,0 +1,11 @@ +variable "environment" { + type = string +} + +variable "region" { + type = string +} + +variable "account" { + type = string +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a3c0997 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +services: + + pandoc: + build: + context: ./ + dockerfile: docker/Dockerfile-pandoc + expose: + - 80 + ports: + - "8081:80" + + volumes: + - pandoc-temp-datatmp:/var/www/datatmp + - pandoc-temp-imgs:/var/www/html/imgs + + hostname: pandoc + domainname: example.com + + restart: unless-stopped + privileged: true + stdin_open: false + tty: false + + livepoll: + build: + context: ./ + dockerfile: docker/Dockerfile-livepoll + expose: + - 80 + ports: + - "8082:3000" + + hostname: livepoll + domainname: example.com + + restart: unless-stopped + privileged: true + stdin_open: false + tty: false + + environment: + LIVEPOLL_PASSWORD: testing + LIVEPOLL_USE_INSECURE_HTTP: true + +volumes: + pandoc-temp-datatmp: + pandoc-temp-imgs: + diff --git a/docker/Dockerfile-livepoll b/docker/Dockerfile-livepoll new file mode 100644 index 0000000..deeb460 --- /dev/null +++ b/docker/Dockerfile-livepoll @@ -0,0 +1,19 @@ +FROM node:22 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends --no-install-suggests \ + jq \ + awscli \ + && apt-get clean + +WORKDIR /app + +COPY livepoll/* /app/ + +RUN npm install + +COPY docker/livepoll-entrypoint.sh /usr/bin/docker-entrypoint.sh + +ENTRYPOINT [ "/usr/bin/docker-entrypoint.sh" ] + +CMD ["node", "/app/index.js"] diff --git a/docker/Dockerfile-pandoc b/docker/Dockerfile-pandoc new file mode 100644 index 0000000..9921665 --- /dev/null +++ b/docker/Dockerfile-pandoc @@ -0,0 +1,20 @@ +FROM php:8.3-apache + +RUN apt-get update \ + && apt-get install -y --no-install-recommends --no-install-suggests \ + pandoc \ + jq \ + awscli \ + && apt-get clean + +COPY --chown=www-data:www-data pandoc/* /var/www/html/ + +RUN mkdir \ + /var/www/datatmp \ + /var/www/html/imgs \ + && chown www-data:www-data \ + /var/www/datatmp \ + /var/www/html/imgs + +# enable php production defaults +RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini \ No newline at end of file diff --git a/docker/livepoll-entrypoint.sh b/docker/livepoll-entrypoint.sh new file mode 100755 index 0000000..b943458 --- /dev/null +++ b/docker/livepoll-entrypoint.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -eo pipefail + +# Required environment variables: +# - PARAMETER_STORE_PREFIX_LIST: Comma-separated list of paths in SSM Parameter Store from which environment variables are pulled. +# If items from multiple paths conflict, the later one is used. + +# https://www.geekcafe.com/blog/how-to-load-parameter-store-values-into-an-ec2s-environment-variables +setup_parameter_store() { + PREFIX=$1 + + parameters=$(aws ssm get-parameters-by-path \ + --path "${PREFIX}" \ + --region ${AWS_REGION} \ + --with-decrypt \ + ) + success=$? + + if [ $success -eq 0 ]; then + # bail if no parameters found + param_count=$(echo "$parameters" | jq '.Parameters | length') + if [ $param_count -eq 0 ]; then + echo "No parameters found in '${PREFIX}!'" + exit 1 + fi + + # split("/")[-1] will grab the last token as the "name" for the env variable + export_statement=$(echo "$parameters" \ + | jq -r '.Parameters[] | "echo \"Pulling from " + .Name + "\"; export " + (.Name | split("/")[-1]) + "=\"" + .Value + "\""' \ + ) + sorted=$(echo "$export_statement" | sort) + + # eval the export strings + eval "$sorted" + else + echo "Failed to retrieve AWS SSM Parameters from '${PREFIX}!'" + exit 1 + fi +} + +if [ ! -z ${PARAMETER_STORE_PREFIX_LIST} ]; then + prefixes=$(echo $PARAMETER_STORE_PREFIX_LIST | tr "," "\n") + + for prefix in $prefixes + do + setup_parameter_store $prefix + done + +else + echo "PARAMETER_STORE_PREFIX_LIST is not set, skipping parameter store setup." +fi + +# Then exec the container's main process (what's set as CMD in the Dockerfile). +exec "$@" \ No newline at end of file diff --git a/pandoc/cleanup-old-files b/pandoc/cleanup-old-files deleted file mode 100644 index 3a06f20..0000000 --- a/pandoc/cleanup-old-files +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -find /var/www/datatmp/* -mtime +1 -exec rm {} \; - -find /var/www/html/imgs/* -mtime +1 -exec rm {} \;