Skip to content

Commit

Permalink
feat(gha): add github action for service update
Browse files Browse the repository at this point in the history
  • Loading branch information
briancaffey committed Feb 28, 2024
1 parent 2ba4968 commit b0820a3
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ad-hoc-base-diff:
cdk diff --app='./lib/examples/ad-hoc/index.js' -e ExampleAdHocBaseStack

ad-hoc-base-deploy:
cdk deploy --app='./lib/examples/ad-hoc/index.js' -e ExampleAdHocBaseStack
cdk deploy --verbose --app='./lib/examples/ad-hoc/index.js' -e ExampleAdHocBaseStack

ad-hoc-base-deploy-approve:
cdk deploy --app='./lib/examples/ad-hoc/index.js' --require-approval never -e ExampleAdHocBaseStack
Expand Down
60 changes: 60 additions & 0 deletions packages/ecs-app-update/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: 'Action for updating ECS service'
description: 'Action for updating ECS service'
author: 'Brian Caffey'
inputs:
BASE_ENV:
required: true
description: 'Base env name (e.g. dev)'
APP_ENV:
required: true
description: 'App env name (e.g. alpha)'
VERSION:
required: true
description: 'Application version git tag (e.g. v1.2.3)'
ECR_REPO:
required: true
description: 'ECR repo to use'
CONTAINER_NAME:
required: true
description: 'Name of the container to update'
AWS_REGION:
required: false
description: 'AWS Region'
default: 'us-east-1'

# Trigger / Inputs
runs:
using: "composite"
steps:
# Note: this assumes that your ECR repo lives in the same AWS account as your ECS cluster
- name: Get current AWS Account
id: get-aws-account
shell: bash
run: |
AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account)
echo "AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID" >> $GITHUB_ENV
- name: Download existing task definition
id: download-task-definition
shell: bash
run: |
aws ecs describe-task-definition \
--task-definition ${{ env.FULL_TASK_NAME }} \
| jq '.taskDefinition' > task-definition.json
- name: Render new task definition
id: render-new-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ inputs.CONTAINER_NAME }}
image: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ inputs.AWS_REGION}}.amazonaws.com/${{ inputs.ECR_REPO }}:${{ inputs.VERSION }}

- name: Deploy new task definition
id: deploy-new-task-definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
cluster: ${{ inputs.APP_ENV }}-cluster
service: ${{ inputs.APP_ENV }}-${{ inputs.CONTAINER_NAME }}
task-definition: ${{ steps.render-new-task-definition.outputs.task-definition }}

61 changes: 39 additions & 22 deletions src/constructs/ad-hoc/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { CfnOutput, Stack } from 'aws-cdk-lib';
import { IVpc, ISecurityGroup } from 'aws-cdk-lib/aws-ec2';
import { Repository } from 'aws-cdk-lib/aws-ecr';
import { Cluster, ContainerImage, EcrImage } from 'aws-cdk-lib/aws-ecs';
import { Cluster, EcrImage } from 'aws-cdk-lib/aws-ecs';
import { IApplicationLoadBalancer, ApplicationListener } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { DatabaseInstance } from 'aws-cdk-lib/aws-rds';
import { CnameRecord, HostedZone } from 'aws-cdk-lib/aws-route53';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { PrivateDnsNamespace } from 'aws-cdk-lib/aws-servicediscovery';
import { Construct } from 'constructs';
// import { HighestPriorityRule } from '../../internal/customResources/highestPriorityRule';
import { EcsRoles } from '../../internal/ecs/iam';
import { ManagementCommandTask } from '../../internal/ecs/management-command';
import { RedisService } from '../../internal/ecs/redis';
import { WebService } from '../../internal/ecs/web';
import { WorkerService } from '../../internal/ecs/worker';

Expand All @@ -20,11 +18,11 @@ export interface AdHocAppProps {
readonly vpc: IVpc;
readonly alb: IApplicationLoadBalancer;
readonly appSecurityGroup: ISecurityGroup;
readonly serviceDiscoveryNamespace: PrivateDnsNamespace;
readonly rdsInstance: DatabaseInstance;
readonly assetsBucket: Bucket;
readonly domainName: string;
readonly listener: ApplicationListener;
readonly elastiCacheHost: string;
}

export class AdHocApp extends Construct {
Expand Down Expand Up @@ -54,20 +52,22 @@ export class AdHocApp extends Construct {
enableFargateCapacityProviders: true,
});

const serviceDiscoveryNamespace = props.serviceDiscoveryNamespace.namespaceName;

const settingsModule = this.node.tryGetContext('config').settingsModule ?? 'backend.settings.production';
// shared environment variables
let environmentVariables: { [key: string]: string } = {
S3_BUCKET_NAME: props.assetsBucket.bucketName,
REDIS_SERVICE_HOST: `${stackName}-redis.${serviceDiscoveryNamespace}`,
REDIS_SERVICE_HOST: props.elastiCacheHost,
POSTGRES_SERVICE_HOST: props.rdsInstance.dbInstanceEndpointAddress,
POSTGRES_NAME: `${stackName}-db`,
POSTGRES_NAME: stackName,
DJANGO_SETTINGS_MODULE: settingsModule,
FRONTEND_URL: `https://${stackName}.${props.domainName}`,
DOMAIN_NAME: props.domainName,
// TODO: read this from ad hoc base stack
DB_SECRET_NAME: 'DB_SECRET_NAME',
APP_ENV_NAME: stackName, // e.g. alpha
BASE_ENV_NAME: props.baseStackName, // e.g. dev
AD_HOC_ENV: 'True', // used in application code
FOO: 'foo',
};

const extraEnvVars = this.node.tryGetContext('config').extraEnvVars;
Expand All @@ -87,18 +87,6 @@ export class AdHocApp extends Construct {
zone: hostedZone,
});

new RedisService(this, 'RedisService', {
cluster,
environmentVariables: {},
vpc: props.vpc,
appSecurityGroup: props.appSecurityGroup,
taskRole: ecsRoles.ecsTaskRole,
executionRole: ecsRoles.taskExecutionRole,
image: ContainerImage.fromRegistry('redis:5.0.3-alpine'),
name: 'redis',
serviceDiscoveryNamespace: props.serviceDiscoveryNamespace,
});

// api service
// const backendService =
new WebService(this, 'ApiService', {
Expand All @@ -110,7 +98,16 @@ export class AdHocApp extends Construct {
executionRole: ecsRoles.taskExecutionRole,
image: backendImage,
listener: props.listener,
command: ['gunicorn', '-t', '1000', '-b', '0.0.0.0:8000', '--log-level', 'info', 'backend.wsgi'],
command: [
'gunicorn',
'-t',
'1000',
'-b',
'0.0.0.0:8000',
'--log-level',
'info',
'backend.wsgi',
],
name: 'gunicorn',
port: 8000,
domainName: props.domainName,
Expand Down Expand Up @@ -148,7 +145,14 @@ export class AdHocApp extends Construct {
taskRole: ecsRoles.ecsTaskRole,
executionRole: ecsRoles.taskExecutionRole,
image: backendImage,
command: ['celery', '--app=backend.celery_app:app', 'worker', '--loglevel=INFO', '-Q', 'default'],
command: [
'celery',
'--app=backend.celery_app:app',
'worker',
'--loglevel=INFO',
'-Q',
'default',
],
name: 'default-worker',
});

Expand All @@ -167,6 +171,19 @@ export class AdHocApp extends Construct {
name: 'backendUpdate',
});

// worker service
new WorkerService(this, 'EcsExecBastion', {
cluster,
environmentVariables,
vpc: props.vpc,
appSecurityGroup: props.appSecurityGroup,
taskRole: ecsRoles.ecsTaskRole,
executionRole: ecsRoles.taskExecutionRole,
image: backendImage,
command: ['sh', '-c', 'tail -f /dev/null'],
name: 'ecs-exec',
});

// define stack output use for running the management command
new CfnOutput(this, 'backendUpdateCommand', { value: backendUpdateTask.executionScript });
new CfnOutput(this, 'domainName', { value: cnameRecord.domainName });
Expand Down
20 changes: 12 additions & 8 deletions src/constructs/ad-hoc/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { IVpc, SecurityGroup } from 'aws-cdk-lib/aws-ec2';
import { ApplicationListener, ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { DatabaseInstance } from 'aws-cdk-lib/aws-rds';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { PrivateDnsNamespace } from 'aws-cdk-lib/aws-servicediscovery';
import { Construct } from 'constructs';
import { AlbResources } from '../../internal/alb';
import { BastionHostResources } from '../../internal/bastion';
import { ElastiCacheCluster } from '../../internal/ec';
import { RdsInstance } from '../../internal/rds';
import { SecurityGroupResources } from '../../internal/sg';
import { ApplicationVpc } from '../../internal/vpc';
Expand All @@ -22,11 +22,11 @@ export class AdHocBase extends Construct {
public alb: ApplicationLoadBalancer;
public appSecurityGroup: SecurityGroup;
public albSecurityGroup: SecurityGroup;
public serviceDiscoveryNamespace: PrivateDnsNamespace;
public databaseInstance: DatabaseInstance;
public assetsBucket: Bucket;
public domainName: string;
public listener: ApplicationListener;
public elastiCacheHostname: string;

constructor(scope: Construct, id: string, props: AdHocBaseProps) {
super(scope, id);
Expand Down Expand Up @@ -58,12 +58,6 @@ export class AdHocBase extends Construct {
this.alb = alb;
this.listener = listener;

const serviceDiscoveryPrivateDnsNamespace = new PrivateDnsNamespace(this, 'ServiceDiscoveryNamespace', {
vpc: this.vpc,
name: `${stackName}-sd-ns`,
});
this.serviceDiscoveryNamespace = serviceDiscoveryPrivateDnsNamespace;

const rdsInstance = new RdsInstance(this, 'RdsInstance', {
vpc: this.vpc,
appSecurityGroup: appSecurityGroup,
Expand All @@ -72,6 +66,16 @@ export class AdHocBase extends Construct {
this.databaseInstance = rdsInstance.rdsInstance;
const { dbInstanceEndpointAddress } = rdsInstance.rdsInstance;

// elasticache cluster
const elastiCacheCluster = new ElastiCacheCluster(this, 'ElastiCacheCluster', {
vpc: this.vpc,
appSecurityGroup: appSecurityGroup,
});

// get the elasticache cluster hostname
this.elastiCacheHostname = elastiCacheCluster.elastiCacheHost;

// TODO: is this needed?
new BastionHostResources(this, 'BastionHostResources', {
appSecurityGroup,
vpc: this.vpc,
Expand Down
68 changes: 68 additions & 0 deletions src/constructs/internal/ec/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { IVpc, Port, SecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2';
import { CfnSubnetGroup, CfnCacheCluster, CfnParameterGroup } from 'aws-cdk-lib/aws-elasticache';
import { Construct } from 'constructs';


interface ElastiCacheClusterProps {
readonly vpc: IVpc;
readonly appSecurityGroup: SecurityGroup;
readonly instanceClass?: string;
readonly instanceSize?: string;
}

export class ElastiCacheCluster extends Construct {
// public rdsInstance: DatabaseInstance;
private instanceClass: string;
private instanceSize: string;
public elastiCacheHost: string;


constructor(scope: Construct, id: string, props: ElastiCacheClusterProps) {
super(scope, id);

// const stackName = Stack.of(this).stackName;

// instance type from props
this.instanceClass = props.instanceClass ?? 't4g';
this.instanceSize = props.instanceSize ?? 'micro';

const cacheNodeType = `cache.${this.instanceClass}.${this.instanceSize}`;

// security group
const elastiCacheSecurityGroup = new SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc,
description: 'Allow all outbound traffic',
allowAllOutbound: true,
});

// elastiCacheSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(6379), 'ElastiCacheRedis');
elastiCacheSecurityGroup.addIngressRule(props.appSecurityGroup, Port.tcp(6379), 'AppSecurityGroup');

// ElastiCache subnet group
const subnetGroup = new CfnSubnetGroup(this, 'SubnetGroup', {
description: 'Subnet group for ElastiCache',
subnetIds: props.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }).subnetIds,
});

// ElastiCache parameter group
const elastiCacheParameterGroup = new CfnParameterGroup(this, 'ElastiCacheParameterGroup', {
description: 'parameter group for elasticache cluster',
cacheParameterGroupFamily: 'redis7',
properties: {},
});

// ElastiCache cluster
const cacheCluster = new CfnCacheCluster(this, 'CacheCluster', {
cacheNodeType: cacheNodeType, // Node type for a single-node cluster
engine: 'redis',
engineVersion: '7.0',
numCacheNodes: 1, // Single node
cacheSubnetGroupName: subnetGroup.ref,
cacheParameterGroupName: elastiCacheParameterGroup.ref,
vpcSecurityGroupIds: [elastiCacheSecurityGroup.securityGroupId],
});

this.elastiCacheHost = cacheCluster.attrRedisEndpointAddress;

}
}
1 change: 1 addition & 0 deletions src/constructs/internal/ecs/iam/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class EcsRoles extends Construct {
});

// S3
// TODO: tighten persmissions https://stackoverflow.com/a/23667874/6084948
taskExecutionRole.addToPolicy(new PolicyStatement({
effect: Effect.ALLOW,
actions: ['s3:*'],
Expand Down
1 change: 0 additions & 1 deletion src/constructs/internal/rds/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// import { Stack } from 'aws-cdk-lib';
import { Stack } from 'aws-cdk-lib';
import { InstanceType, IVpc, Peer, Port, SecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2';
import { Credentials, DatabaseInstance, DatabaseInstanceEngine, PostgresEngineVersion } from 'aws-cdk-lib/aws-rds';
Expand Down
6 changes: 3 additions & 3 deletions src/examples/ad-hoc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ const addHocApp = new AdHocApp(appStack, 'AdHocApp', {
vpc: adHocBase.vpc,
alb: adHocBase.alb,
appSecurityGroup: adHocBase.appSecurityGroup,
serviceDiscoveryNamespace: adHocBase.serviceDiscoveryNamespace,
rdsInstance: adHocBase.databaseInstance,
assetsBucket: adHocBase.assetsBucket,
domainName: adHocBase.domainName,
listener: adHocBase.listener,
elastiCacheHost: adHocBase.elastiCacheHostname,
});

/**
* Add tagging for this construct and all child constructs
*/
Tags.of(adHocBase).add('env', adHocBaseEnvName);
Tags.of(addHocApp).add('env', adHocAppEnvName);
Tags.of(adHocBase).add('base-env', adHocBaseEnvName);
Tags.of(addHocApp).add('app-env', adHocAppEnvName);

0 comments on commit b0820a3

Please sign in to comment.